Skip to content

test#70

Closed
MonaaEid wants to merge 1 commit intomainfrom
Test-consensus
Closed

test#70
MonaaEid wants to merge 1 commit intomainfrom
Test-consensus

Conversation

@MonaaEid
Copy link
Owner

No description provided.

@github-actions
Copy link

Hi, this is WorkflowBot.
Your pull request cannot be merged as it is not passing all our workflow checks.
Please click on each check to review the logs and resolve issues so all checks pass.
To help you:

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Walkthrough

This PR introduces two new TCK handler modules (consensus and key) for topic creation and key generation operations, updates SDK client initialization to support legacy node setups and DER-encoded keys, and enables Flask debug mode in the server startup.

Changes

Cohort / File(s) Summary
Handler Module Registration
tck/handlers/__init__.py
Added imports for consensus and key modules to trigger their @register_handler decorators, expanding the handler registry without modifying the public all list.
New Consensus Handler
tck/handlers/consensus.py
New handler for TopicCreateTransaction with comprehensive field validation (memo length/null-bytes, autoRenewPeriod range, key parsing). Retrieves client from session, builds transaction with optional fields, signs, executes, and validates receipt status before returning topic ID.
New Key Handler
tck/handlers/key.py
New handler supporting multiple key generation modes (ed25519/secp256k1 private/public keys, KeyList, ThresholdKey). Includes DER serialization, extensive parameter validation, and returns hex-encoded keys with optional private key lists.
SDK Client Enhancement
tck/handlers/sdk.py
Enhanced _create_client to accept params and support legacy TCK setups via nodeIp/nodeAccountId. Updated _parse_operator_credentials to attempt DER-encoded key parsing before hex string fallback and return a typed Tuple. Added Tuple to typing imports.
Server Configuration
tck/server.py
Enabled debug mode (debug=True) when starting the Flask application.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author. Add a description explaining the purpose and scope of the changes, such as the new handlers being added and their functionality.
Title check ❓ Inconclusive The title 'test' is vague and generic, providing no meaningful information about the changeset which adds consensus and key handler modules. Use a descriptive title that summarizes the main change, such as 'Add consensus and key handler modules' or 'Implement TCK handlers for topic creation and key generation'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Test-consensus

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 22cb49ee-f332-49c3-a953-3835c7263769

📥 Commits

Reviewing files that changed from the base of the PR and between 51218c0 and 683be42.

📒 Files selected for processing (5)
  • tck/handlers/__init__.py
  • tck/handlers/consensus.py
  • tck/handlers/key.py
  • tck/handlers/sdk.py
  • tck/server.py

Comment on lines +29 to +37
try:
# Try to parse as a private key first
try:
return PrivateKey.from_string(key_string)
except Exception:
# If it fails, try as a public key
return PublicKey.from_string(key_string)
except Exception as e:
raise JsonRpcError(INTERNAL_ERROR, f"Failed to parse key: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider using INVALID_PARAMS instead of INTERNAL_ERROR for key parsing failures.

When a user provides an invalid key string, this is an input validation error rather than an internal server error. Using INTERNAL_ERROR may mislead clients about the nature of the failure.

♻️ Proposed fix
     except Exception as e:
-        raise JsonRpcError(INTERNAL_ERROR, f"Failed to parse key: {str(e)}")
+        raise JsonRpcError(INVALID_PARAMS, f"Invalid key format: {str(e)}")

Comment on lines +57 to +58
except Exception as e:
raise JsonRpcError(INTERNAL_ERROR, f"Failed to parse signer key: {str(e)}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Same error code concern: Consider using INVALID_PARAMS for signer key parsing failures.

Consistent with the _parse_key function, invalid signer keys are user input errors.

♻️ Proposed fix
         except Exception as e:
-            raise JsonRpcError(INTERNAL_ERROR, f"Failed to parse signer key: {str(e)}")
+            raise JsonRpcError(INVALID_PARAMS, f"Invalid signer key format: {str(e)}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
raise JsonRpcError(INTERNAL_ERROR, f"Failed to parse signer key: {str(e)}")
except Exception as e:
raise JsonRpcError(INVALID_PARAMS, f"Invalid signer key format: {str(e)}")

try:
period = int(params["autoRenewPeriod"])
# Validate auto-renew period (approximate range: 6999999 to 8000001)
if period <= 0 or period < 6999999 or period > 8000001:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Redundant condition in auto-renew period validation.

The condition period <= 0 or period < 6999999 is redundant since period < 6999999 already covers all non-positive values.

♻️ Proposed simplification
-                if period <= 0 or period < 6999999 or period > 8000001:
+                if period < 6999999 or period > 8000001:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if period <= 0 or period < 6999999 or period > 8000001:
if period < 6999999 or period > 8000001:

Comment on lines +117 to +118
except ValueError:
raise JsonRpcError(INVALID_PARAMS, "Invalid params", "autoRenewPeriod must be a string representing an integer")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Missing exception chaining.

The raise statement discards the original exception context. Use raise ... from None if intentional, or preserve the chain for debugging.

♻️ Proposed fix to preserve exception chain
             except ValueError:
-                raise JsonRpcError(INVALID_PARAMS, "Invalid params", "autoRenewPeriod must be a string representing an integer")
+                raise JsonRpcError(INVALID_PARAMS, "Invalid params", "autoRenewPeriod must be a string representing an integer") from None

Comment on lines +11 to +26
def _key_to_hex(key: Any) -> str:
"""Convert a key to its DER-encoded hex representation.

Args:
key: PrivateKey or PublicKey instance

Returns:
Hex string representation of the key
"""
proto_key = key_to_proto(key)
if proto_key is None:
raise JsonRpcError(INTERNAL_ERROR, "Failed to convert key to protobuf")

# Serialize to bytes and convert to hex
key_bytes = proto_key.SerializeToString()
return key_bytes.hex().upper()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify key_to_proto extracts public key from private key
ast-grep --pattern $'def key_to_proto($$$):
    $$$
'

Repository: MonaaEid/hiero-sdk-python

Length of output: 51


🏁 Script executed:

cd /tmp && find . -name "key_utils.py" -type f 2>/dev/null | head -5

Repository: MonaaEid/hiero-sdk-python

Length of output: 51


🏁 Script executed:

rg "def key_to_proto" --type py -A 10

Repository: MonaaEid/hiero-sdk-python

Length of output: 1056


🏁 Script executed:

rg "key_to_proto" --type py -B 2 -A 5 | head -50

Repository: MonaaEid/hiero-sdk-python

Length of output: 3238


🏁 Script executed:

sed -n '30,50p' src/hiero_sdk_python/utils/key_utils.py

Repository: MonaaEid/hiero-sdk-python

Length of output: 712


🏁 Script executed:

cat src/hiero_sdk_python/utils/key_utils.py | head -80

Repository: MonaaEid/hiero-sdk-python

Length of output: 1739


🏁 Script executed:

cat -n tck/handlers/key.py | sed -n '50,70p'

Repository: MonaaEid/hiero-sdk-python

Length of output: 1049


🏁 Script executed:

cat -n tck/handlers/key.py | sed -n '110,165p'

Repository: MonaaEid/hiero-sdk-python

Length of output: 3241


🏁 Script executed:

rg "def to_string_der" --type py -A 3

Repository: MonaaEid/hiero-sdk-python

Length of output: 665


🏁 Script executed:

rg "class PrivateKey" --type py -A 20 | head -50

Repository: MonaaEid/hiero-sdk-python

Length of output: 1629


🏁 Script executed:

rg "def to_string_der\|def to_bytes_raw\|def to_string_raw" src/hiero_sdk_python/crypto/private_key.py -A 5

Repository: MonaaEid/hiero-sdk-python

Length of output: 51


🏁 Script executed:

cat -n src/hiero_sdk_python/crypto/private_key.py | grep -A 10 "def to_string_der"

Repository: MonaaEid/hiero-sdk-python

Length of output: 469


🏁 Script executed:

cat -n src/hiero_sdk_python/crypto/private_key.py | sed -n '386,400p'

Repository: MonaaEid/hiero-sdk-python

Length of output: 614


Critical: _key_to_hex returns public key hex for private keys, not private key hex.

The key_to_proto function in src/hiero_sdk_python/utils/key_utils.py (lines 38-46) extracts and converts the public key when a PrivateKey is passed: return key.public_key()._to_proto(). This means _key_to_hex(private_key) serializes the public key, not the private key.

This affects:

  • Lines 57-58: ed25519PrivateKey endpoint
  • Lines 65-66: ecdsaSecp256k1PrivateKey endpoint
  • Lines 115, 119: keyList private key serialization
  • Lines 158, 162: thresholdKey private key serialization

Use PrivateKey.to_string_der() for private key serialization:

🐛 Proposed fix
 def _key_to_hex(key: Any) -> str:
-    """Convert a key to its DER-encoded hex representation.
+    """Convert a key to its protobuf-serialized hex representation.
+    
+    Note: For PrivateKey, this returns the PUBLIC key's protobuf serialization,
+    as key_to_proto extracts the public key. Use PrivateKey.to_string_der() for private keys.
     
     Args:
         key: PrivateKey or PublicKey instance

Then update private key handlers:

         if key_type == "ed25519PrivateKey":
             private_key = PrivateKey.generate_ed25519()
+            private_key_hex = private_key.to_string_der()
             return {
-                "key": _key_to_hex(private_key),
-                "privateKeys": [_key_to_hex(private_key)]
+                "key": private_key_hex,
+                "privateKeys": [private_key_hex]
             }

Apply same fix to ecdsaSecp256k1PrivateKey, and update keyList/thresholdKey private key appends to use private_key.to_string_der() instead of _key_to_hex(private_key).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _key_to_hex(key: Any) -> str:
"""Convert a key to its DER-encoded hex representation.
Args:
key: PrivateKey or PublicKey instance
Returns:
Hex string representation of the key
"""
proto_key = key_to_proto(key)
if proto_key is None:
raise JsonRpcError(INTERNAL_ERROR, "Failed to convert key to protobuf")
# Serialize to bytes and convert to hex
key_bytes = proto_key.SerializeToString()
return key_bytes.hex().upper()
def _key_to_hex(key: Any) -> str:
"""Convert a key to its protobuf-serialized hex representation.
Note: For PrivateKey, this returns the PUBLIC key's protobuf serialization,
as key_to_proto extracts the public key. Use PrivateKey.to_string_der() for private keys.
Args:
key: PrivateKey or PublicKey instance
Returns:
Hex string representation of the key
"""
proto_key = key_to_proto(key)
if proto_key is None:
raise JsonRpcError(INTERNAL_ERROR, "Failed to convert key to protobuf")
# Serialize to bytes and convert to hex
key_bytes = proto_key.SerializeToString()
return key_bytes.hex().upper()

Comment on lines +69 to +97
elif key_type == "ed25519PublicKey":
# Get or generate Ed25519 public key
if "fromKey" in params and params["fromKey"]:
# Derive public key from private key
private_key = PrivateKey.from_string(params["fromKey"])
public_key = private_key.public_key()
else:
# Generate new key pair and return public key
private_key = PrivateKey.generate_ed25519()
public_key = private_key.public_key()

return {
"key": _key_to_hex(public_key)
}

elif key_type == "ecdsaSecp256k1PublicKey":
# Get or generate ECDSA (secp256k1) public key
if "fromKey" in params and params["fromKey"]:
# Derive public key from private key
private_key = PrivateKey.from_string(params["fromKey"])
public_key = private_key.public_key()
else:
# Generate new key pair and return public key
private_key = PrivateKey.generate_ecdsa_secp256k1()
public_key = private_key.public_key()

return {
"key": _key_to_hex(public_key)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if PrivateKey has methods to determine key type
ast-grep --pattern $'class PrivateKey:
    $$$
    def is_ed25519($$$):
        $$$
    $$$
}'

Repository: MonaaEid/hiero-sdk-python

Length of output: 51


🏁 Script executed:

# First, let's explore the repository structure and find where PrivateKey is imported from
cat tck/handlers/key.py | head -30

Repository: MonaaEid/hiero-sdk-python

Length of output: 1082


🏁 Script executed:

# Also search for the PrivateKey class definition
fd -t f -e py | xargs rg "class PrivateKey" -l

Repository: MonaaEid/hiero-sdk-python

Length of output: 111


🏁 Script executed:

# Look for the from_string method definition
rg "def from_string" -A 5 -B 2

Repository: MonaaEid/hiero-sdk-python

Length of output: 12219


🏁 Script executed:

sed -n '1,100p' src/hiero_sdk_python/crypto/private_key.py

Repository: MonaaEid/hiero-sdk-python

Length of output: 3707


🏁 Script executed:

# Look for the full from_string method
sed -n '/def from_string/,/^[[:space:]]*def /p' src/hiero_sdk_python/crypto/private_key.py | head -50

Repository: MonaaEid/hiero-sdk-python

Length of output: 1619


🏁 Script executed:

# Check if PrivateKey has any methods/properties to determine its type
rg "def.*type|self\._.*type|self\.key_type" src/hiero_sdk_python/crypto/private_key.py

Repository: MonaaEid/hiero-sdk-python

Length of output: 231


🏁 Script executed:

# View the specific lines 69-97 in the handler file
sed -n '69,97p' tck/handlers/key.py

Repository: MonaaEid/hiero-sdk-python

Length of output: 1342


🏁 Script executed:

# Find and examine the from_bytes method
sed -n '/def from_bytes/,/^[[:space:]]*def /p' src/hiero_sdk_python/crypto/private_key.py | head -80

Repository: MonaaEid/hiero-sdk-python

Length of output: 2012


🏁 Script executed:

# Also check if PrivateKey has a method to get/determine its type
rg "def.*_key_type|def.*is_ed|def.*is_ecdsa|_private_key\.__class__" src/hiero_sdk_python/crypto/private_key.py

Repository: MonaaEid/hiero-sdk-python

Length of output: 134


🏁 Script executed:

# Check the full context around lines 69-97 and see if there's any validation
sed -n '50,110p' tck/handlers/key.py

Repository: MonaaEid/hiero-sdk-python

Length of output: 2650


🏁 Script executed:

# Also look for the is_ed25519 and is_ecdsa method implementations
sed -n '/def is_ed25519/,/^[[:space:]]*def /p' src/hiero_sdk_python/crypto/private_key.py | head -30

Repository: MonaaEid/hiero-sdk-python

Length of output: 348


🏁 Script executed:

# Check is_ecdsa as well
sed -n '/def is_ecdsa/,/^[[:space:]]*def /p' src/hiero_sdk_python/crypto/private_key.py | head -30

Repository: MonaaEid/hiero-sdk-python

Length of output: 342


Use key-type-specific loaders and validate key types when deriving from fromKey.

When fromKey is provided for ecdsaSecp256k1PublicKey, the code uses PrivateKey.from_string(), which defaults to Ed25519 for 32-byte keys (trying Ed25519 first, then ECDSA). This means a 32-byte ECDSA private key will be silently interpreted as Ed25519, producing an incorrect public key.

Fix: For ecdsaSecp256k1PublicKey, use PrivateKey.from_string_ecdsa() instead of PrivateKey.from_string(). Alternatively, validate the loaded key type using private_key.is_ed25519() or private_key.is_ecdsa() matches the requested type.

Comment on lines +108 to +125
for key_spec in keys_spec:
if not isinstance(key_spec, dict):
raise JsonRpcError(INVALID_PARAMS, "Each key spec must be a dict")

spec_type = key_spec.get("type")
if spec_type == "ed25519PrivateKey":
private_key = PrivateKey.generate_ed25519()
private_keys_hex.append(_key_to_hex(private_key))
proto_key = key_to_proto(private_key)
elif spec_type == "ecdsaSecp256k1PrivateKey":
private_key = PrivateKey.generate_ecdsa_secp256k1()
private_keys_hex.append(_key_to_hex(private_key))
proto_key = key_to_proto(private_key)
else:
raise JsonRpcError(INVALID_PARAMS, f"Unsupported key type in keyList: {spec_type}")

if proto_key:
key_list_proto.keys.append(proto_key)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Code duplication in key spec parsing.

The key spec parsing logic for keyList (lines 108-125) and thresholdKey (lines 151-168) is nearly identical. Consider extracting a helper function to reduce duplication.

♻️ Proposed refactoring
def _parse_key_spec(key_spec: Dict[str, Any]) -> tuple[PrivateKey, Any]:
    """Parse a key spec and return (private_key, proto_key)."""
    if not isinstance(key_spec, dict):
        raise JsonRpcError(INVALID_PARAMS, "Each key spec must be a dict")
    
    spec_type = key_spec.get("type")
    if spec_type == "ed25519PrivateKey":
        private_key = PrivateKey.generate_ed25519()
    elif spec_type == "ecdsaSecp256k1PrivateKey":
        private_key = PrivateKey.generate_ecdsa_secp256k1()
    else:
        raise JsonRpcError(INVALID_PARAMS, f"Unsupported key type: {spec_type}")
    
    return private_key, key_to_proto(private_key)

Also applies to: 151-168

config = ServerConfig()
logger.info(f"Starting TCK server on {config.host}:{config.port}")
app.run(host=config.host, port=config.port)
app.run(host=config.host, port=config.port, debug=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security concern: Debug mode enabled in Flask.

Enabling debug=True exposes the Werkzeug interactive debugger, which can execute arbitrary Python code if an attacker can trigger an exception. This is a security risk if this server is accessible beyond localhost.

Consider making debug mode configurable via environment variable or disabling it for non-development environments:

🛡️ Proposed fix to make debug mode configurable
-    app.run(host=config.host, port=config.port, debug=True)
+    debug_mode = os.getenv("TCK_DEBUG", "false").lower() == "true"
+    app.run(host=config.host, port=config.port, debug=debug_mode)

@github-actions
Copy link

Hello, this is the OfficeHourBot.

This is a reminder that the Hiero Python SDK Office Hours are scheduled in approximately 4 hours (14:00 UTC).

This session provides an opportunity to ask questions regarding this Pull Request.

Details:

Disclaimer: This is an automated reminder. Please verify the schedule here for any changes.

From,
The Python SDK Team

@github-actions
Copy link

Hi there! I'm the LinkedIssueBot.
This pull request has been automatically closed due to the following reason(s):

Thank you,
From Python SDK team

@github-actions github-actions bot closed this Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant